02 - 四种形态:库、进程、扩展、插件
数据快照 2026-08-19。目录结构与文件大小均为当天
gh api实测。
选网关的第一个决策不是"选哪家",而是**"它以什么形式存在于你的系统里"**。这个决定一旦做出,后面的能力边界基本就锁死了 —— 因为形态决定了它能拿到什么、能改什么、挂了会怎样。
前置:01 - 网关是什么。
本篇回答:同样叫"网关",为什么有的是一个 Python 库、有的是一个独立进程、有的是别人代理里的一个插件?这个选择会锁死你后面的能力边界。
会用到的词(不熟悉不影响读,遇到时回来查):
- Envoy:CNCF 的开源代理,K8s 生态里事实上的流量层标准
- ext_proc:Envoy 的外部处理机制 —— 把请求交给一个外部 gRPC 服务,让它改完再还回来
- Wasm / proxy-wasm:把插件编译成 WebAssembly 跑在代理进程内的沙箱里,proxy-wasm 是它和代理之间的接口规范
- sidecar:和业务容器部署在一起、共享网络的辅助进程
一、四种形态一眼看懂
| 形态 | 代表 | 网络跳数 | 挂了会怎样 | 能改请求体吗 | |
|---|---|---|---|---|---|
| ① | 库 | LiteLLM SDK | 0 | 业务一起挂 | 能,同进程 |
| ② | 独立进程 | LiteLLM Proxy / agentgateway | +1 | 全站不可用 | 能 |
| ③ | Envoy 扩展 | Envoy AI Gateway | +1(gRPC 旁路) | 可配置为 fail-open | 能 |
| ④ | Wasm 插件 | Higress | 0(代理内) | 插件级隔离 | 能,但受 ABI 限制 |
二、形态一:库 —— LiteLLM SDK
LiteLLM 是唯一一个同时提供库和进程两种形态的,而且这两种形态共用同一份路由内核。
代码组织上,litellm/router.py 一个文件 560 KB,是整个项目的心脏;litellm/proxy/proxy_server.py 728 KB,是把这个心脏包成 HTTP 服务的那层壳。
litellm/
├── router.py 560 KB ← 路由内核,库和进程共用
├── router_strategy/ ← 六种路由策略,第 03 篇细拆
├── proxy/
│ ├── proxy_server.py 728 KB ← FastAPI 应用
│ ├── auth/ ← 虚拟密钥、JWT,第 04 篇细拆
│ ├── hooks/ ← 限流、预算
│ └── guardrails/ ← 内容安全(安全专题细拆)
└── ...
库形态的好处是零网络开销,代价是语言绑定。 你的业务如果是 Java 或 Go,这条路直接堵死 —— 这也是为什么 LiteLLM 明明是库出身,实际生产部署里绝大多数人用的是它的 Proxy 形态。
一个反直觉的细节:库形态其实是安全上最脆弱的。2026 年 3 月 LiteLLM 的 PyPI 供应链事件里,恶意版本 1.82.8 通过 litellm_init.pth 实现了"任意 Python 进程启动即执行"—— 只要这个包装在环境里,哪怕你根本没 import,payload 也跑了。而官方 Docker 镜像的用户完全没受影响,因为镜像的 requirements.txt 钉死了依赖版本。 形态选择在这里直接变成了爆炸半径的差异。(完整复盘放在安全专题)
三、形态二:独立进程 —— agentgateway
agentgateway 是 Rust 写的独立数据面,代码按 crate 分层:
crates/
├── agentgateway/ ← 主体
│ └── src/
│ ├── proxy/ ← 代理核心
│ ├── llm/ ← LLM 协议转换与策略
│ ├── mcp/ ← MCP 协议处理(第 05 篇细拆)
│ ├── a2a/ ← Agent-to-Agent
│ ├── cel/ ← CEL 表达式引擎,用来写策略
│ ├── transport/ ← 传输层
│ └── telemetry/ ← 可观测
├── llm/ ← 各家 provider 的请求体转换
├── http/
├── pool/ ← 连接池
├── hbone/ ← HTTP-Based Overlay Network Environment
└── cel-fork/ ← 自己 fork 的 CEL 实现
有三个设计信号值得注意:
1. a2a/ 和 mcp/ 是并列的一等公民。 大部分 AI 网关只管"业务 → 模型"这一段流量,agentgateway 从目录结构上就把"Agent → 工具"(MCP)和"Agent → Agent"(A2A)当成同等重要的流量类型。这是"LLM 网关"和"Agent 网关"的分水岭。
2. 它 fork 了 CEL。 crates/cel-fork/ 的存在说明策略引擎不是拿来主义 —— 策略要在数据面热路径上跑,性能和语义都得自己控。CEL(Common Expression Language)在这里承担的是"用一行表达式描述一条鉴权/路由规则"。
3. hbone/ 是从 Istio 带过来的。 agentgateway 的主要贡献者和 Istio / kgateway 生态高度重叠,这个 crate 基本坐实了它的技术血统。
四、形态三:Envoy 扩展 —— Envoy AI Gateway
Envoy AI Gateway 把自己定位成 Envoy Gateway 的 AI 扩展,代码分成"控制面"和"数据面扩展"两块:
internal/
├── controller/ ← K8s controller,把 CRD 翻译成 Envoy 配置
│ ├── gateway.go 56 KB
│ ├── mcp_route.go 40 KB
│ └── mcp_route_security_policy.go 37 KB
├── extensionserver/ ← 给 Envoy Gateway 打补丁的扩展服务
│ ├─ ─ post_translate_modify.go 49 KB
│ └── quota_ratelimit.go 35 KB
├── extproc/ ← 数据面:Envoy External Processing
│ └── processor_impl.go 43 KB
├── translator/ ← 各家 provider 的请求体互转
├── apischema/ ← OpenAI / Anthropic / Bedrock 的 schema 定义
│ └── openai/openai.go 354 KB ← 单文件最大
├── mcpproxy/ ← MCP 代理(第 05 篇细拆)
└── tracing/openinference/ ← OTel GenAI 语义约定落地
这个形态的关键机制是 ext_proc:Envoy 在处理请求时,通过 gRPC 把请求头/请求体交给一个外部进程,外部进程可以修改后再还给 Envoy。internal/extproc/processor_impl.go(43 KB)就是这个外部进程的实现。
好处:所有 Envoy 已有的能力(TLS、连接池、熔断、可观测、mTLS)全部白拿,AI 相关逻辑只写增量部分。 代价:配置全部通过 K8s CRD 表达。不跑 Kubernetes 的话,这条路基本没法走。
注意 apischema/openai/openai.go 有 354 KB —— 这是四家里单文件最大的。它说明了一件事:"兼容 OpenAI 格式"这句话的实际成本,是把一份持续膨胀的 schema 手工维护在你自己的仓库里。
五、形态四:Wasm 插件 —— Higress
Higress 走的是第四条路:能力做成 Wasm 插件,跑在 Envoy 进程内的 Wasm VM 里。
plugins/wasm-go/extensions/
├── ai-proxy/ ← 协议转换,各家 provider
│ └── provider/
│ ├── provider.go 65 KB
│ ├── failover.go 27 KB ← 第 03 篇细拆
│ ├── claude_to_openai.go 46 KB
│ └── {bedrock,vertex,gemini,qwen,...}.go
├── ai-statistics/ ← token 统计 60 KB
├── ai-token-ratelimit/ ← 按 token 限流
├── ai-cache/ ← 语义缓存
├── ai-security-guard/ ← 内容安全
├── ai-search/
└── ai-agent/
一个能力一个插件,按需装载。 这是四家里粒度最细的组织方式,也最符合"网关的能力应该可插拔"这个直觉。
但 Wasm 形态有一个很硬的约束,在源码里看得很清楚:Wasm VM 之间不共享内存,只能通过 proxy-wasm 的 shared data 通信,而且要用 CAS 保证一致性。 这导致一件在其他形态里根本不是问题的事 —— "谁来执行后台健康检查" —— 在这里必须靠分布式租约解决:
// plugins/wasm-go/extensions/ai-proxy/provider/failover.go
type Lease struct {
VMID string `json:"vmID"`
Timestamp int64 `json:"timestamp"`
}
func (c *ProviderConfig) tryAcquireOrRenewLease(vmID string) bool {
now := time.Now().Unix()
data, cas, err := proxywasm.GetSharedData(c.failover.ctxVmLease)
// ...
// If vmID is itself, try to renew the lease directly
// If the lease is expired (60s), try to acquire the lease
if lease.VMID == vmID || now-lease.Timestamp > 60 {
lease.VMID = vmID
lease.Timestamp = now
return c.setLease(vmID, now, cas)
}
return false
}
租约 60 秒过期,setLease 通过 proxywasm.SetSharedData(key, value, cas) 做 CAS 写入,文件顶部还定义了 casMaxRetries = 10。
这段代码是整篇文章最值得记住的地方:一个"给挂掉的 API Key 做健康检查"的需求,在库形态里是十行代码,在 Wasm 形态里变成了一个需要处理 CAS 冲突和租约过期的分布式协调问题。形态的代价就体现在这种地方。
六、怎么选
再叠加一条与形态无关的判断:如果 MCP 流量是你的主要场景(Agent 要连一堆外部工具),那 agentgateway 和 Envoy AI Gateway 是仅有的两个把 MCP 做成一等公民的选择,第 05 篇会把这两家的实现摆在一起读。
下一篇 → 02 - 路由与容错:把 LiteLLM 的六种路由策略和 Higress 的故 障转移逐行读一遍,看"选哪个后端"这个决策到底怎么算出来的。